24 错误处理

C语言的弱项:错误的检测和处理并不是c语言的强项

  • C语言对运行时错误以多种形式表示,而没有提供一种统一的方式
  • 程序员必须将检测错误的代码编写在程序代码中,因此很容易忽略一些错误

扩展:C++语言对C语言的这一弱点进行了改进,提供了一种新的错误错误的方式-异常处理(exception handling)。

24.1 assert.h:诊断

assert宏(函数)

断言:一个我们认为在正常情况下一顶为真的表达式。
错误信息:标准C要求在显示的消息中指明以下内容

  • 传递给assert函数的参数
  • 包括assert调用的文件名
  • assert调用所在的行号

说明:检查断言,如果值不为0,会向stderr输出一条信息,并调用abort函数终止程序。
技巧:因为引入了额外的检查,因此会增加程序的运行时间。可以在测试没问题后通过NDEBUG(宏)进制assert调用。
注意:因为assert可能会被禁用,因此不要在assert调用中使用有副作用的表达式。
原型assert.h

1
2
3
4
/**
* @param {int} expresstion 断言
*/

void assert(int expression);
1
2
3
a[i];
assert(0 <= i && i < N);// 保证下标不回溢出
a [i] = 0;

禁止assert调用

1
2
#define NDEBUG// 值不重要,定义了就行
#include <assert.h>

24.2 errno.h:错误

说明:除了EDOMERANGE,还定义了其他宏,这是合法的,但命名要遵循C标准,即E数组或大写字母

errno宏


描述:如果确实是宏,C语言标准要求它表示左值4.2.2,以便和变量一样使用。
说明:如果确实是宏,C语言标准要求它表示左值4.2.2,以便和普通变量一样使用。
用途:函数被调用后会会为errno赋值,如果errno不为0,代表函数调用过程中有错误发生。
应用:大部分使用errno变量的函数集中在math.h,也有一些在标准库的其他部分。
相关宏:EDOMERANGE,errno中存储的值通常是这两个宏

宏(errno的值) 错误类型 说明
EDOM 定义域错误 传递给函数的一个参数不属于函数的定义域
ERANGE 取值范围错误 函数的返回值太大,无法用double
1
2
3
4
5
6
7
8
errno = 0;

// 调用库函数
y = sqrt(x);

if (errno != 0) {
fprintf(stderr, "sqrt")
}

perror函数


描述:stderr输出一条错误信息。
错误信息:sqrt error: Math argument(定义域错误)

perror的参数 分号 空格 出错消息 换行符
sqrt error : Math argument

原型:stdio.h

1
2
3
4
/**
* @param {char *} s 错误描述
*/

void perror(const char *s);
1
2
3
4
5
6
7
8
9
10
errno = 0;

// 调用sqrt
y = sqrt(x);

// 检测运行状况
if (errno != 0) {
perror("sqrt error");
exit(EXIT_FAILURE);
}

strerror函数


描述:根据错误类型行值返回指向错误字符串的指针。
原型:string.h

1
2
3
4
5
/**
* @param {int} errnum 错误类型值
* @return {char *} 对应的错误字符串
*/

char *strerror(int errnum);

24.3 signal.h:信号处理

信号(signal):处理异常情况的工具

  • 运行时错误

    例如:除以0

  • 程序以外导致的事件

    例如:许多操作系统都允许用户终端或终止运行的程序

异步的:它们可以在程序执行过程中的任意时刻发生,而不仅是在程序员所知道的特定时刻发生

24.3.1 信号宏

兼容性:C标准不要求下面列表中的信号都会发生,大多数C语言的实现都至少支持其中一部分。

宏名 含义
SIGABRT 异常终止(可能由于调用abort导致)
SIGFPE 在数学运算中发生错误(可能是除以0或溢出)
SIGILL 非法指令
SIGINT 中断
SIGSEGV 非法存储访问
SIGTERM 终止请求

24.3.2 signal函数


描述:为指定信号注册指定处理函数。
相关宏:SIG_ERR

说明:当注册失败时会返回该值
用途:检测注册处理函数是否成功

特点

多对一:信号与处理函数是多对一的关系

  • 可以对多种信号绑定同一个处理函数,处理函数可以根据传入的参数(信号类型)决定进行哪种操作
  • 也可以对同一个信号注册多个处理程序,但前面注册的会被后面注册的处理函数覆盖。

同步性:发出信号的行为是异步的,但处理函数处理的过程是同步的。也就是说,注册了处理函数的信号出现后,程序会暂停并执行信号处理函数,返回后暂停的程序从信号发生点恢复并继续执行。

信号 处理函数返回后程序行为
SIGABRT 终止
SIGFPE 处理函数返回后的程序行为未定义
其它 暂停的程序从信号发生点恢复并继续执行

一次性:信号处理完之后,除非处理函数被重新注册,否则该信号不回被同一个函数处理两遍。
无限递归问题:如果信号是由处理这个信号的函数引发的,如果没有其它机制将会发生无限递归。所以,C语言要求,除了SIGTLL,当一个信号的处理函数被调用时,该信号对应的处理函数要么要被重置为SIG_DFL或以其它方式加以封锁。

限制:处理函数和普通函数相比多了一些限制

可以 不可以
可以忽略该信号 自由调用库函数
执行一些错误修复 访问静态存储期限的变量
终止程序
可以调用signal,只要第一个参数为正被处理的信号
调用库函数,只要信号处理函数是由raise或abort调用的

原型:signal.h

1
2
3
4
5
6
/**
* @param {int} sig 信号类型
* @param {func *} func 处理函数
* @return {func *} 指向注册过同样信号的上一个处理函数的指针:成功;SIG_ERR:注册失败(同时会在errno中存储一个正值)
*/

void *(*signal)(int sig, void (*func)(int));// 书上写法是:void (*signal(int sig, void (*func)(int)))(int);
1
2
3
4
5
6
7
8
9
10
11
12
// 声明函数指针
void (*orig_handler)(int);

// 为中断注册处理函数,并将之前的处理函数存储下来
orig_handler = signal(SIGINT, handler);

if (orig_handler == SIG_ERR) {
// 注册处理函数失败
}

// 换成原来的处理函数
signal(SIGINT, SIG_DFL);

24.3.3 预定义的信号处理函数

说明:出了编写我们自己的信号处理函数,我们还可以选择使用signal.h提供的预定义的处理函数。
预定义的信号处理函数命名规则:

SIG_大写字母

SIG_DFL宏(函数)


说明:按“默认”的方式处理
描述:行为由实现定义,大多数情况下会导致程序终止。
原型:signal.h

1
2
3
4
/**
* @param {int} signal 信号类型
*/

void SIG_DFL(int signal);
1
signal(SIGINT, SIG_IGN); // 使用默认行为响应“中断”信号

SIG_IGN宏(函数)


描述:什么都不做,忽略信号
原型:signal.h

1
2
3
4
/**
* @param {int} signal 信号类型
*/

void SIG_IGN(int signal);

24.3.4 raise函数


说明:触发信号
原型:signal.h

1
2
3
4
5
/**
* @param {int} sig 信号类型
* @return {int} 0:成功;非0:失败
*/

int raise(int sig);
1
2
// 触发SIGABORT
raise(SIGABOUT);

24.3.5 程序:测试信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Tests signals
*/


#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

void handler(int sig);
void raise_sig(void);

int main () {
// 声明函数指针
void (*orig_handler)(int);

/*1. 第一次实验*/
// 注册处理函数
printf("---first--- %d\n", SIGILL);
signal(SIGILL, handler);
// 触发信号
raise_sig();

/*2. 第二次实验*/
// 再次注册,忽略相应信号
printf("---second---\n");
orig_handler = signal(SIGILL, SIG_IGN);
raise_sig();

/*3. 第三次实验*/
printf("---third---\n");
// 更改为第一次实验使用的处理函数
signal(SIGILL, orig_handler);
raise_sig();
return 0;
}

void handler (int sig) {
printf("Handler called for signal %d\n", sig);
}

void raise_sig (void) {
raise(SIGILL);
}
1
2
3
4
5
6
$ ./tsignal
---first--- 4
Handler called for signal 4
---second---
---third---
Handler called for signal 4

24.4 setjmp.h:非局部跳转

说明:通常情况下,函数调用后会回到它被调用的位置。但setjmp.h提供了使一个函数直接跳转到另一个函数(而且不需要返回)的方式。
goto:只能配合标记实现局部跳转,也就是在同一个函数内部跳转。

setjmp宏(函数)

描述:标记程序中的一个“位置”
应用:生成标记位置,稍后提供给longjmp函数
限制:按照标准C,只有两种使用setjmp的方式是合法的(否则不具备可移植性)

  • 作为表达式语句(可能会前置转换成void)
  • 作为if、switch、while、do、for语句中控制表达式的一部分(constexp:计算结果为整数的常量表达式op关系判等运算符)
  1. setjmp(…)
  2. !setjmp(…)
  3. constexp op constexp
  4. setjmp(…) op constexp

原型:setjmp.h

1
2
3
4
5
/**
* @param {jmp_buf} env 用来保存生成的被调用时所处的“位置”(数组)
* @return {int} 0:第一次调用时返回;非0:longjmp将控制权重新转给最初的setjmp宏调用,后者这次的返回值非零
*/

int setjmp(jmp_buf env);

longjmp函数


描述:首先根据参数env的值恢复当前环境,然后从setjmp宏调用中返回
注意:一定要确保参数env已经被setjmp宏初始化了,否则程序可能会崩溃。
应用:可以由多种潜在的用途,但主要被用于错误处理。
原型:setjmp.h

1
2
3
4
5
/**
* @param {jmp_buf} env
* @param {int} val
*/

void longjmp(jmp_buf env, int val);

程序:测试setjmp和longjmp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Tests setjmp/longjmp
*/


#include <setjmp.h>
#include <stdio.h>

/**
* @type {jmp_buf} env 存储位置数据的全局变量
*/

static jmp_buf env;

void f1 (void);
void f2 (void);

int main () {
int ret;
// 获得存储位置
ret = setjmp(env);
printf("setjmp returned %d\n", ret);

// 第二次执行到这里的时候值不为0
if (ret != 0) {
printf("Program terminates: longjmp called\n");
return 0;
}

f1();

// 下面这句不会执行
printf("Program terminates normally\n");
return 0;
}

void f1 (void) {
printf("f1 begins\n");
f2();
printf("f1 returns\n");
}
void f2 (void) {
printf("f2 begins\n");
// 按照env的值跳转到指定的环境
longjmp(env, 1);
printf("f2 returns\n");
}
1
2
3
4
5
6
$ ./tsetjmp               
setjmp returned 0
f1 begins
f2 begins
setjmp returned 1
Program terminates: longjmp called